package entity;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.MapKey;
import javax.persistence.MapKeyEnumerated;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.UniqueConstraint;

import entity.Fase.FaseRUP;
import entity.Iteracao.SituacaoIteracao;
import exception.AlmiranteException;

@Entity
@Table(name="projeto", schema="almirante",
		uniqueConstraints={@UniqueConstraint(columnNames={"nome"})})
@NamedQueries({ @NamedQuery(name = "listaProjetosPorGerente", query = "FROM Projeto projeto WHERE projeto.gerenteProjetos.idRecurso = :idRecurso"),

@NamedQuery(name = "listaProjetosPorRecurso", 
	query = "SELECT DISTINCT p from Projeto p, Recurso r " +
			"WHERE r.idRecurso = :idRecurso " +
			"AND (p.gerenteProjetos = r OR r MEMBER OF p.recursos) " +
			"AND p.acessoRapido = true " +
			"ORDER BY p.dataCriacao DESC"),
@NamedQuery(name="listaProjetosPorNome", query = "FROM Projeto p WHERE p.nome = :nome")
})
public class Projeto {
	@Id
	@SequenceGenerator(name="projeto_seq", sequenceName="projeto_id_projeto_seq",schema="almirante")
	@GeneratedValue(strategy=GenerationType.AUTO, generator="projeto_seq")
	@Column(name="id_projeto")
	private Long id;
	
	@Column(name="nome", nullable=false, length=80)
	private String nome;
	
	@Temporal(TemporalType.TIMESTAMP)
	@Column(name="data_criacao")
	private Date dataCriacao;
	
	@Enumerated
	@Column(name="situacao", nullable=false)
	private SituacaoProjeto situacao;
	
	@Embedded
	private Periodo periodo;
	
	@OneToMany(mappedBy="projeto", fetch=FetchType.EAGER, cascade=CascadeType.ALL, orphanRemoval=true)
	@MapKeyEnumerated
	@MapKey(name="id.faseRup")
	private Map<Fase.FaseRUP, Fase> fases = new HashMap<Fase.FaseRUP, Fase>();
	
	@ManyToOne
	@JoinColumn(name="id_recurso", referencedColumnName="id_recurso")
	private Recurso gerenteProjetos;
	
	@Column(name="motivo_cancelamento", length=1000)
	private String motivoCancelamento;
	
	@Column(name="acesso_rapido")
	private Boolean acessoRapido;
	
	@OneToMany(mappedBy="projeto")
	@MapKey(name="nome")
	private Map<String, Requisito> requisitos = new HashMap<String, Requisito>();
	
	@OneToMany(mappedBy="projeto")
	private List<CasoUso> listCasoUso = new ArrayList<CasoUso>();
	
	@ManyToMany
	@JoinTable(name="projeto_recurso", schema="almirante", 
		joinColumns=@JoinColumn(name="id_projeto"), 
		inverseJoinColumns=@JoinColumn(name="id_recurso"))
	private Set<Recurso> recursos = new HashSet<Recurso>();
	
	public static enum SituacaoProjeto {
		CRIADO("Criado"), 
		EM_PLANEJAMENTO("Em Planejamento"), 
		EM_ANDAMENTO("Em Andamento"), 
		CONCLUIDO("Concluído"),
		CANCELADO("Cancelado");
		
		public String descricao;
		
		private SituacaoProjeto(String descricao) {
			this.descricao = descricao;
		}
		
		public String getDescricao() {
			return descricao;
		}
	}
	
	// Construtores
	Projeto() {}
	
	public Projeto(String nome, Periodo periodo) {
		this.nome = nome;
		this.dataCriacao = new Date();
		this.periodo = periodo;
		this.situacao = SituacaoProjeto.CRIADO;
		adicionarFase(FaseRUP.INICIACAO);
		adicionarFase(FaseRUP.ELABORACAO);
		adicionarFase(FaseRUP.CONSTRUCAO);
		adicionarFase(FaseRUP.TRANSICAO);
	}

	public void adicionarFase(FaseRUP faseRup) {
		if (possuiFase(faseRup))
			throw new AlmiranteException("Projeto já possui a fase de " + faseRup.nome);
		
		if (!podeAdicionarIteracaoNaFase(faseRup))
			throw new AlmiranteException("Não é possível adicionar fase de " + faseRup.getNome() + " ao projeto, pois" +
					" existe(m) fase(s) subsequente(s) com iteração(ões) Em Andamento, Planejada(s) ou Concluída(s).");
		
		fases.put(faseRup, new Fase(this, faseRup));
	}

	public Date getFimPlanejado() {
		Fase ultimaFase = fases.get(fasesOrdenadas().lastKey());
		return ultimaFase.getFim();
	}

	public boolean possuiFase(FaseRUP faseRup) {
		return fases.containsKey(faseRup);
	}

	public void removerFase(FaseRUP faseRup) {
		if (fases.size() == 1)
			throw new AlmiranteException("Não é possível remover a fase. Projeto precisa possuir pelo menos uma fase.");
		
		if (possuiFase(faseRup)) {
			Fase fase = fases.get(faseRup);
			if (fase.possuiIteracao())
				throw new AlmiranteException("Fase de " + faseRup.nome + " não pode ser removida por possuir iterações");
			fases.remove(faseRup);
		}
	}
	
	public void adicionarIteracao(FaseRUP faseRup, Iteracao iteracao) {
		if (!this.situacao.equals(SituacaoProjeto.EM_PLANEJAMENTO))
			throw new AlmiranteException("Para adicionar uma iteração, o projeto precisa estar Em Planejamento");
		
		if (!possuiFase(faseRup))
			throw new AlmiranteException("Projeto não possui fase de " + faseRup.nome);
		
		if (!podeAdicionarIteracaoNaFase(faseRup))
			throw new AlmiranteException("Não é possível adicionar iterações na fase " + faseRup.nome + 
					" pois a fase posterior possui iteração(ões) Em Andamento, Planejada(s) ou Concluída(s). ");
		
		getFase(faseRup).adicionarIteracao(iteracao);
		recalcularPeriodoIteracoes();
	}
	
	public Fase getFase(FaseRUP faseRup) {
		return fases.get(faseRup);
	}

	protected Iteracao getIteracao(int indice) {
		List<Iteracao> iteracoes = getIteracoes();
		
		if (!iteracoes.isEmpty()) {
			if (iteracoes.size() <= indice)
				throw new AlmiranteException("Projeto não possui iteração #" + indice);
			return iteracoes.get(indice);			
		}
		return null;
	}
	
	public List<Iteracao> getIteracoes() {
		List<Iteracao> iteracoes = new ArrayList<Iteracao>();
		for (Fase f : fasesOrdenadas().values()) {
			iteracoes.addAll(f.getIteracoes());
		}
		return Collections.unmodifiableList(iteracoes);
	}

	public boolean possuiIteracaoNaFase(FaseRUP faseRup) {
		Fase f = fases.get(faseRup);
		if (f != null)
			return f.possuiIteracao();
		
		return false;
	}
	
	public void removerIteracao(FaseRUP faseRup, Integer numero) {
		if (!situacao.equals(SituacaoProjeto.EM_PLANEJAMENTO))
			throw new AlmiranteException("Projeto não está EM PLANEJAMENTO. Não é possível remover iterações");
		
		getFase(faseRup).removerIteracao(numero);
		recalcularPeriodoIteracoes();
	}
	
	private SortedMap<Fase.FaseRUP, Fase> fasesOrdenadas() {
		return new TreeMap<Fase.FaseRUP, Fase>(this.fases);
	}
	
	public Iteracao getIteracao(FaseRUP faseRup, Integer sequencial) {
		return getFase(faseRup).getIteracao(sequencial);
	}
	
	Periodo periodoIteracao(Iteracao iteracao) {
		List<Iteracao> iteracoes = getIteracoes();
		if (!iteracoes.contains(iteracao))
			throw new AlmiranteException("Iteração não pertence ao projeto");
		
		Iteracao anterior = !iteracao.equals(iteracoes.get(0)) ? 
				iteracoes.get(iteracoes.indexOf(iteracao) - 1) : null;
		
		if (anterior == null) 
			return new Periodo(getInicio(), iteracao.getDuracao());
		
		return anterior.getPeriodoEstimado().novoPeriodoPosterior(iteracao.getDuracao());
	}
	
	void recalcularPeriodoIteracoes() {
		for (Iteracao iteracao : getIteracoes()) {
			iteracao.setPeriodoEstimado(periodoIteracao(iteracao));
		}
	}
	
	void recalcularPeriodoIteracoesPosterioresA(Iteracao iteracao, int diferenca) {
		List<Iteracao> iteracoes = getIteracoes();
		if (!iteracoes.contains(iteracao))
			throw new AlmiranteException("Iteração não pertence ao projeto");
	
		int index = iteracoes.indexOf(iteracao);
		List<Iteracao> iteracoesPosteriores = iteracoes.subList(index, iteracoes.size() - 1);
		for (Iteracao it : iteracoesPosteriores) {
			it.setPeriodoEstimado(it.getPeriodoEstimado().adiantarEmDias(diferenca));
		}
	}
	
	public void planejar() {
		this.situacao = SituacaoProjeto.EM_PLANEJAMENTO;
	}
	
	public void finalizarPlanejamento() {
		if (possuiFaseSemIteracao())
			throw new AlmiranteException("Não é possível finalizar o planejamento enquanto o projeto possuir fases sem iterações.");
		this.situacao = SituacaoProjeto.EM_ANDAMENTO;
		this.periodo = periodo.novoFim(getFimPlanejado());
	}
	
	private boolean possuiFaseSemIteracao() {
		for (Fase f : fases.values()) {
			if (!f.possuiIteracao())
				return true;
		}
		
		return false;
	}
	
	public boolean podeSerPlanejado() {
		return ((situacao.equals(SituacaoProjeto.EM_PLANEJAMENTO) || situacao.equals(SituacaoProjeto.CRIADO))
				|| possuiIteracaoASerPlanejada()) && !situacao.equals(SituacaoProjeto.CANCELADO);
	}
	
	public Integer getHorasEstimadas() {
		int horas = 0;	
		for (Fase fase : fases.values()) {
			horas += fase.getHorasEstimadas();
		}
		
		return horas;
	}
	
	public Integer getHorasRealizadas() {
		int horas = 0;	
		for (Fase fase : fases.values()) {
			horas += fase.getHorasRealizadas();
		}
		
		return horas;
	}
	
	public void adicionarRequisito(Requisito requisito) {
		if (requisitos.containsKey(requisito.getNome()))
			throw new AlmiranteException("Projeto já possui requisito com nome: " + requisito.getNome());
		requisito.setProjeto(this);
		requisitos.put(requisito.getNome(), requisito);
	}
	
	public void adicionarCasoUso(CasoUso casoUso) {
		if (listCasoUso == null || listCasoUso.contains(casoUso)) {
			throw new AlmiranteException("Projeto já possui requisito com nome: " + casoUso.getNomeCasoUso());
		}
		else {
			casoUso.setProjeto(this);
			listCasoUso.add(casoUso);
		}
	}
	
	public boolean possuiRequisito(Requisito requisito) {
		return requisitos.containsKey(requisito.getNome());
	}
	
	public Iteracao proximaIteracaoNaoPlanejada() {
		List<Iteracao> iteracoes = getIteracoes();
		if (!iteracoes.isEmpty()) {
			for (Iteracao iteracao : iteracoes) {
				if (iteracao.getSituacao().equals(SituacaoIteracao.CRIADA)
						|| iteracao.getSituacao().equals(SituacaoIteracao.EM_PLANEJAMENTO)) {
					return iteracao;
				}
			}
		}
		return null;
	}
	
	List<Iteracao> iteracoesAnterioresA(Iteracao iteracao) {
		List<Iteracao> iteracoesAnteriores = new ArrayList<Iteracao>();
		for (Iteracao it : getIteracoes()) {
			if (it.equals(iteracao))
				break;
			iteracoesAnteriores.add(it);
		}
		
		return iteracoesAnteriores;
	}
	
	private Fase proximaFase(FaseRUP faseRup) {
		for (FaseRUP f : FaseRUP.values()) {
			if (f.ordinal() > faseRup.ordinal()) {
				if (possuiFase(f)) {
					return getFase(f);
				}
			}
		}
		
		return null;
	}
	
	private boolean podeAdicionarIteracaoNaFase(FaseRUP faseRup) {
		Fase proximaFase = proximaFase(faseRup);
		if (proximaFase != null) {
			if (proximaFase.possuiIteracao()) {
				for (Iteracao iteracao : proximaFase.getIteracoes()) {
					if (iteracao.getSituacao().equals(SituacaoIteracao.EM_ANDAMENTO)
							|| iteracao.getSituacao().equals(SituacaoIteracao.PLANEJADA)
							|| iteracao.getSituacao().equals(SituacaoIteracao.CONCLUIDA)) {
						return false;
					}
				}
			}
		}
		
		return true;
	}
	
	public Iteracao iteracaoCorrente() {
		List<Iteracao> iteracoes = getIteracoes();
		if (!iteracoes.isEmpty()) {
			for (Iteracao iteracao : iteracoes) {
				if (!iteracao.getSituacao().equals(SituacaoIteracao.CONCLUIDA)) {
					return iteracao;
				}
			}
		}
		return null;
	}
	
	public String getNome() {
		return nome;
	}
	
	public void setNome(String nome) {
		this.nome = nome;
	}

	public Date getDataCriacao() {
		return dataCriacao;
	}
	
	public SituacaoProjeto getSituacao() {
		return situacao;
	}
	
	public Date getInicio() {
		return periodo.getInicio();
	}
	
	public List<Fase> getFases() {
		return new ArrayList<Fase>(fasesOrdenadas().values());
	}

	public Date getFim() {
		return periodo.getFim();
	}
	
	public Long getId() {
		return this.id;
	}
	
	public Date getInicioRealizado() {
		List<Iteracao> iteracoes = getIteracoes();
		if (!iteracoes.isEmpty()) {
			return iteracoes.get(0).getInicioRealizado();
		}
		
		return null;
	}
	
	public Date getFimRealizado() {
		Iteracao ultimaIteracao = ultimaIteracao();
		if (ultimaIteracao != null)
			return ultimaIteracao.getFimRealizado();
		
		return null;
	}
	
	Iteracao ultimaIteracao() {
		List<Iteracao> iteracoes = getIteracoes();
		if (!iteracoes.isEmpty()) {
			return iteracoes.get(iteracoes.size() - 1);
		}
		return null;
	}
	
	void concluir() {
		situacao = SituacaoProjeto.CONCLUIDO;
	}
	
	public void setPeriodo(Periodo periodo) {
		if (!podeAlterarPeriodo()) {
			throw new AlmiranteException("Período do projeto não pode ser alterado, pois " +
					"já possui iteração(ões) Em Andamento, Concluída ou Em Replanejamento.");
		}
		
		this.periodo = periodo;
		recalcularPeriodoIteracoes();
	}
	
	public Periodo getPeriodo() {
		return periodo;
	}
	
	private boolean possuiIteracaoNaSituacao(SituacaoIteracao... situacao) {
		List<Iteracao> iteracoes = getIteracoes();
		if (!iteracoes.isEmpty()) {
			List<SituacaoIteracao> listSituacao = Arrays.asList(situacao);
			for (Iteracao iteracao : iteracoes) {
				if (listSituacao.contains(iteracao.getSituacao())) {
					return true;
				}
			}
		}
		return false;
	}
	
	public boolean podeAlterarPeriodo() {
		return !possuiIteracaoNaSituacao(SituacaoIteracao.EM_ANDAMENTO, SituacaoIteracao.CONCLUIDA, SituacaoIteracao.EM_REPLANEJAMENTO);
	}
	
	public boolean possuiIteracaoASerPlanejada() {
		return proximaIteracaoNaoPlanejada() != null;
	}
	
	public void cancelar(String motivoCancelamento) {
		if (!podeSerCancelado())
			throw new AlmiranteException("Projeto não pode ser cancelado, pois já se encontra " + situacao.descricao);
		this.motivoCancelamento = motivoCancelamento;
		situacao = SituacaoProjeto.CANCELADO;
	}
	
	public boolean podeSerCancelado() {
		return ! (situacao.equals(SituacaoProjeto.CONCLUIDO) || situacao.equals(SituacaoProjeto.CANCELADO));
	}
	
	public String getMotivoCancelamento() {
		return motivoCancelamento;
	}
	
	public Boolean getAcessoRapido() {
		return acessoRapido;
	}

	public void setAcessoRapido(Boolean acessoRapido) {
		this.acessoRapido = acessoRapido;
	}

	public Recurso getGerenteProjetos() {
		return gerenteProjetos;
	}

	public void setGerenteProjetos(Recurso gerenteProjetos) {
		this.gerenteProjetos = gerenteProjetos;
	}

	boolean possuiCasoUso(CasoUso casoUso) {
		return listCasoUso.contains(casoUso);
	}
	
	public void adicionarRecurso(Recurso recurso) {
		recursos.add(recurso);
	}
	
	public boolean possuiRecurso(Recurso recurso) {
		return recursos.contains(recurso) || recurso.equals(gerenteProjetos);
	}
	
	public void removerRecurso(Recurso recurso) {
		List<Iteracao> iteracoes = getIteracoes();
		if (!iteracoes.isEmpty()) {
			for (Iteracao iteracao : iteracoes) {
				if (iteracao.possuiRecurso(recurso)) {
					throw new AlmiranteException(recurso.getNomeRecurso() + " não pode ser removido do projeto, pois já está vinculado à " +
							"iteração #" + iteracao.getNumero() + " da fase de " + iteracao.getFase().getFaseRup().nome);
				}
			}
		}
		
		recursos.remove(recurso);
	}
	
	public boolean ehOGerente(Recurso recurso) {
		return gerenteProjetos.equals(recurso);
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((dataCriacao == null) ? 0 : dataCriacao.hashCode());
		result = prime * result + ((nome == null) ? 0 : nome.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Projeto other = (Projeto) obj;
		if (dataCriacao == null) {
			if (other.dataCriacao != null)
				return false;
		} else if (!dataCriacao.equals(other.dataCriacao))
			return false;
		if (nome == null) {
			if (other.nome != null)
				return false;
		} else if (!nome.equals(other.nome))
			return false;
		return true;
	}
	
	
}
